package jmine.tec.persist.web.pages.auth;

import images.ImageResources;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import jmine.tec.web.pages.HelpTextMessages;
import jmine.tec.web.wicket.BeanReference;
import jmine.tec.web.wicket.component.InlinedCheckBox;
import jmine.tec.web.wicket.component.button.ActionButton;
import jmine.tec.web.wicket.component.paging.PagingNavigatorContainer;
import jmine.tec.web.wicket.model.DefaultDetachableModel;
import jmine.tec.web.wicket.pages.Template;
import jmine.tec.web.wicket.spring.SpringBeanReference;

import org.apache.wicket.Component;
import org.apache.wicket.PageParameters;
import org.apache.wicket.RequestCycle;
import org.apache.wicket.ResourceReference;
import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
import org.apache.wicket.extensions.markup.html.repeater.data.table.DataTable;
import org.apache.wicket.extensions.markup.html.repeater.data.table.HeadersToolbar;
import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.form.IChoiceRenderer;
import org.apache.wicket.markup.html.image.Image;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.IDataProvider;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.spring.injection.annot.SpringBean;
import org.hibernate.EntityMode;
import org.hibernate.SessionFactory;
import org.hibernate.metadata.ClassMetadata;

import bancosys.tec.exception.BusinessException;
import bancosys.tec.exception.MessageCreator;
import bancosys.tec.persist.PersistenceController;
import bancosys.tec.persist.authorization.Authorizable;
import bancosys.tec.persist.authorization.AuthorizationContext;
import bancosys.tec.persist.authorization.annotation.DisplayName;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.persist.dao.BaseDAOFactory;
import bancosys.tec.persist.hibernate.PartitionSessionFactory;
import bancosys.tec.persist.hibernate.page.PagingData;
import bancosys.tec.persist.hibernate.page.PagingService;
import bancosys.tec.security.annotation.Secure;
import bancosys.tec.security.authorization.UrlPermission;
import bancosys.tec.security.chinesewall.ChineseWallContext;
import bancosys.tec.security.chinesewall.ChineseWallContextHolder;

/**
 * @author takeshi
 */
@Secure(id = "URL_AUTORIZACAO", permissionType = UrlPermission.class)
public class Authorization extends Template {
    private static final int RESULTS_PER_PAGE = 10;

    private static volatile List<Class<? extends Authorizable>> persistables;

    public static final String AUTHORIZABLE_KEY = "authorizable";

    private Class<? extends Authorizable> authorizationClass;

    @SpringBean
    private PagingService pagingService;

    @SpringBean
    private PartitionSessionFactory sessionFactory;

    @SpringBean
    private BaseDAOFactory baseDAOFactory;

    private final BeanReference<PersistenceController> persistenceControllerRef = SpringBeanReference.forName("persistenceController");

    private final Set<Serializable> chosen = new HashSet<Serializable>();

    /**
     * 
     */
    public Authorization() {
        super();
        this.authorizationClass = null;
        this.init();
    }

    /**
     * @param parameters {@link PageParameters}
     * @throws ClassNotFoundException e
     */
    @SuppressWarnings("unchecked")
    public Authorization(PageParameters parameters) throws ClassNotFoundException {
        super(parameters);
        this.authorizationClass = (Class<? extends Authorizable>) Class.forName(parameters.getString(AUTHORIZABLE_KEY));
        this.init();
    }

    /**
     * 
     */
    private void init() {
        this.add(new FeedbackPanel("feedback"));
        final Form<Void> form = new Form<Void>("authForm") {
            @Override
            protected void onSubmit() {
                @SuppressWarnings("unchecked")
                final FormComponent<Class<?>> component = (FormComponent<Class<?>>) this.get("authorizables");
                final Class<?> modelObject = component.getModelObject();
                if (modelObject != null) {
                    Authorization.this.changeAuthorizableClass(modelObject);
                }
            }
        };
        form.setMarkupId("formAutorizacao");

        Link refresh = new Link("linkRefresh") {
            @Override
            public void onClick() {
                RequestCycle.get().setRedirect(true);
                this.setResponsePage(Authorization.class);
            }
        };
        refresh.add(new Image("refreshIcon", new ResourceReference(ImageResources.class, "refresh.gif")));
        form.add(refresh);

        final IChoiceRenderer<Class<?>> renderer = new IChoiceRenderer<Class<?>>() {

            public Object getDisplayValue(Class<?> object) {
                DisplayName displayName = object.getAnnotation(DisplayName.class);
                return displayName.value();
            }

            public String getIdValue(Class<?> object, int index) {
                return object.getName();
            }
        };
        final DropDownChoice<Class<?>> persistablesChoice =
                new DropDownChoice<Class<?>>("authorizables", new Model<Class<?>>(this.authorizationClass),
                        this.getClassesInAuthorization(), renderer) {
                    @Override
                    protected void onSelectionChanged(Class<?> newSelection) {
                        Authorization.this.authorizationClass = null;
                    }
                };
        persistablesChoice.setNullValid(true);
        persistablesChoice.setMarkupId("authorizables");
        persistablesChoice.setOutputMarkupId(true);
        form.add(persistablesChoice);
        this.add(form);
        final Button butAuth = new ActionButton("buttonAuthorize", new Model<String>("Autorizar")) {
            @Override
            public void onAction() {
                Authorization.this.authorizeOrReject(true);
            }
        };
        form.add(butAuth);
        final Button butRej = new ActionButton("buttonReject", new Model<String>("Rejeitar")) {
            @Override
            public void onAction() {
                Authorization.this.authorizeOrReject(false);
            }
        };
        form.add(butRej);

        if (this.authorizationClass != null && persistablesChoice.getChoices().contains(this.authorizationClass)) {
            DataTable<Authorizable> table =
                    new DataTable<Authorizable>("entities", this.getColumns(), this.getDataProvider(), RESULTS_PER_PAGE);
            table.addTopToolbar(new HeadersToolbar(table, null));
            PagingNavigatorContainer paging = new PagingNavigatorContainer("navigator", table);
            form.add(table);
            form.add(paging);
        } else {
            WebMarkupContainer cont = new WebMarkupContainer("entities");
            cont.setVisible(false);
            PagingNavigatorContainer paging = new PagingNavigatorContainer("navigator", new EmptyPageable());
            paging.setVisible(false);
            form.add(cont);
            form.add(paging);
            butAuth.setVisible(false);
            butRej.setVisible(false);
        }
    }

    /**
     * @param authorize se <code>true</code>, autoriza
     */
    private void authorizeOrReject(boolean authorize) {
        String authorizeOrReject = authorize ? "autorizado" : "rejeitado";

        boolean commit = false;
        PersistenceController persistenceController = this.persistenceControllerRef.get();
        persistenceController.startTransaction();
        try {
            BaseDAO<Authorizable> dao = this.baseDAOFactory.getGenericDAO(this.authorizationClass);
            int i = 0;
            for (Serializable pk : this.chosen) {
                if (authorize) {
                    dao.findByPk(pk).getAuthorizer().authorize();
                } else {
                    dao.findByPk(pk).getAuthorizer().reject();
                }
                i++;
            }

            if (i == 0) {
                this.getSession().getFeedbackMessages().error(null, "Nenhum registro selecionado para ser " + authorizeOrReject + "!");
            } else if (i == 1) {
                this.getSession().getFeedbackMessages().info(null, "1 registro " + authorizeOrReject + " com sucesso!");
            } else if (i > 1) {
                this.getSession().getFeedbackMessages().info(null, i + " registros " + authorizeOrReject + "s com sucesso!");
            }
            commit = true;
        } catch (final Exception e) {
            throw new BusinessException(PersistWebMessages.ERRO_AUTORIZACAO.create(), e);
        } finally {
            if (commit) {
                persistenceController.commit();
            } else {
                persistenceController.rollback();
            }
        }
    }

    /**
     * Devolve o {@link IDataProvider}
     * 
     * @return {@link IDataProvider} com os beans em autorizacao
     */
    private IDataProvider<Authorizable> getDataProvider() {
        return new IDataProvider<Authorizable>() {

            public void detach() {
                this.dao = null;
            }

            public BaseDAO<Authorizable> getDao() {
                if (this.dao == null) {
                    this.dao = Authorization.this.baseDAOFactory.getGenericDAO(Authorization.this.authorizationClass);
                }
                return this.dao;
            }

            private BaseDAO<Authorizable> dao;

            public int size() {
                Authorization.this.changeAuthorizationContext(AuthorizationContext.cold);
                try {
                    return this.getDao().countBeansToAuthorize();
                } finally {
                    Authorization.this.changeAuthorizationContext(AuthorizationContext.hot);
                }
            }

            public IModel<Authorizable> model(final Authorizable object) {
                return new DefaultDetachableModel<Authorizable>(object);
            }

            public Iterator<? extends Authorizable> iterator(int first, int count) {
                PagingData pagingData = Authorization.this.pagingService.getPagingData();
                Authorization.this.changeAuthorizationContext(AuthorizationContext.cold);
                try {
                    PagingData np = new PagingData();
                    np.setTotalItens(count);
                    np.setCurrentPage(first / RESULTS_PER_PAGE);
                    np.setItensPerPage(RESULTS_PER_PAGE);
                    Authorization.this.pagingService.setPagingData(np);
                    return this.getDao().findBeansToAuthorize().iterator();
                } finally {
                    Authorization.this.pagingService.setPagingData(pagingData);
                    Authorization.this.changeAuthorizationContext(AuthorizationContext.hot);
                }
            }
        };
    }

    /**
     * Model que adiciona os elementos ao set {@link Authorization#chosen}
     * 
     * @author takeshi
     */
    private final class SelectAuthorizablesModel implements IModel<Boolean> {
        private final Serializable primaryKey;

        /**
         * @param primaryKey {@link Serializable}
         */
        public SelectAuthorizablesModel(Serializable primaryKey) {
            this.primaryKey = primaryKey;
        }

        /**
         * {@inheritDoc}
         */
        public Boolean getObject() {
            return Authorization.this.chosen.contains(this.primaryKey);
        }

        /**
         * {@inheritDoc}
         */
        public void setObject(Boolean object) {
            if (object.booleanValue()) {
                Authorization.this.chosen.add(this.primaryKey);
            } else {
                Authorization.this.chosen.remove(this.primaryKey);
            }
        }

        /**
         * {@inheritDoc}
         */
        public void detach() {
        }
    }

    /**
     * Cria as colunas
     * 
     * @return array de {@link IColumn}
     */
    @SuppressWarnings("unchecked")
    private IColumn<Authorizable>[] getColumns() {
        List<DisplayNameColumn> columns = new ArrayList<DisplayNameColumn>();
        for (Method method : this.authorizationClass.getMethods()) {
            if (this.includeMethod(method)) {
                columns.add(new DisplayNameColumn(method));
            }
        }
        Collections.sort(columns);
        IColumn[] array = new IColumn[columns.size() + 1];
        // must add at index 0 a select
        array[0] = new IColumn<Authorizable>() {

            public Component getHeader(String componentId) {
                return new Label(componentId, "'");
            }

            public String getSortProperty() {
                return null;
            }

            public boolean isSortable() {
                return false;
            }

            public void populateItem(Item<ICellPopulator<Authorizable>> cellItem, String componentId, final IModel<Authorizable> rowModel) {
                final InlinedCheckBox checkBox =
                        new InlinedCheckBox(componentId, new SelectAuthorizablesModel(rowModel.getObject().getPk()));
                cellItem.add(checkBox);
            }

            public void detach() {
            }
        };
        for (int i = 1; i < array.length; i++) {
            array[i] = columns.get(i - 1);
        }
        return array;
    }

    /**
     * @param method {@link Method}
     * @return boolean
     */
    private boolean includeMethod(Method method) {
        return method.isAnnotationPresent(DisplayName.class);
    }

    /**
     * @param class1 {@link Class}
     */
    private void changeAuthorizableClass(Class<?> class1) {
        RequestCycle.get().setRedirect(true);
        final PageParameters pp = new PageParameters();
        pp.add(AUTHORIZABLE_KEY, class1.getName());
        RequestCycle.get().setResponsePage(Authorization.class, pp);
    }

    /**
     * @return {@link List}
     */
    private List<? extends Class<?>> getClassesInAuthorization() {
        Collection<Class<? extends Authorizable>> authorizableClasses = this.getAuthorizableClasses(this.sessionFactory);
        this.changeAuthorizationContext(AuthorizationContext.cold);
        try {
            List<Class<?>> list = new ArrayList<Class<?>>();
            for (Class<? extends Authorizable> type : authorizableClasses) {
                BaseDAO<Authorizable> dao = this.baseDAOFactory.getGenericDAO(type);
                if (dao.countBeansToAuthorize() > 0) {
                    list.add(type);
                }
            }
            return list;
        } finally {
            this.changeAuthorizationContext(AuthorizationContext.hot);
        }
    }

    /**
     * @param sessionFactory2 the {@link SessionFactory}
     * @return {@link List}
     */
    @SuppressWarnings("unchecked")
    private List<Class<? extends Authorizable>> getAuthorizableClasses(SessionFactory sessionFactory2) {
        if (persistables == null) {
            List<Class<? extends Authorizable>> cl = new ArrayList<Class<? extends Authorizable>>();
            Collection<ClassMetadata> mts = sessionFactory2.getAllClassMetadata().values();
            for (ClassMetadata classMetadata : mts) {
                Class<?> concrete = classMetadata.getMappedClass(EntityMode.POJO);
                if (Authorizable.class.isAssignableFrom(concrete) && concrete.isAnnotationPresent(DisplayName.class)) {
                    DisplayName annotation = concrete.getAnnotation(DisplayName.class);
                    if (annotation.showAuth()) {
                        cl.add((Class<? extends Authorizable>) concrete);
                    }
                }
            }
            final Comparator<Class<?>> cmp = new Comparator<Class<?>>() {

                public int compare(Class<?> o1, Class<?> o2) {
                    String a1 = o1.getAnnotation(DisplayName.class).value();
                    String a2 = o2.getAnnotation(DisplayName.class).value();
                    return a1.compareTo(a2);
                }
            };
            Collections.sort(cl, cmp);
            persistables = cl;
        }
        return persistables;
    }

    /**
     * Troca o contexto da autorização.
     * 
     * @param authorizationContext contexto da autorização (quente e frio)
     * @return novo contexto da autorização.
     */
    public AuthorizationContext changeAuthorizationContext(AuthorizationContext authorizationContext) {

        ChineseWallContext chineseWallContext = ChineseWallContextHolder.get();
        if (chineseWallContext != null) {
            if (authorizationContext.equals(AuthorizationContext.cold)) {
                chineseWallContext.disable();
            } else {
                chineseWallContext.enable();
            }
        }

        PartitionSessionFactory partitionSessionFactory = (PartitionSessionFactory) this.persistenceControllerRef.get().getSessionFactory();
        return this.sessionFactory.changeSessionAuthorizationContext(partitionSessionFactory.getCurrentSession(), authorizationContext);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected MessageCreator getHelpTextCreator() {
        return HelpTextMessages.AUTHORIZATION;
    }
}
